pr_10_sales_funnel_analysis¶
Analysis of the mobile app sales funnel based on the results of the A/B test.
Анализ воронки продаж мобильного приложения¶
Проясним, как ведут себя пользователи мобильного приложения по продаже продуктов питания и выберем лучший шрифт.
Ключевые задачи:
- Изучим воронку продаж:
- Узнаем, как пользователи доходят до покупки;
- Посчитаем количество пользователей, которые доходят до покупки;
- Количество тех, кто «застревает» на предыдущих шагах. Поймем на каких именно;
- Исследуем результаты A/A/B-эксперимента, посвященного выбору лучшего шрифта.
План работ:
Шаг 1. Получим данные
Откроем файл с данными и изучим общую информацию.
Шаг 2. Подготовим данные
- Исправим названия столбцов;
- Проверим пропуски и типы данных;
- Добавим столбец даты и времени, а также отдельный столбец дат;
Шаг 3. Изучим и проверим данные
- Вычислим:
- Количество событий в логе;
- Количества пользователей в логе;
- Среднее количество событий на пользователя;
- Поймем, данными за какой период мы располагаем?
- Как меняется количество данных в зависимости от времени в разрезе групп.
- Можно ли быть уверенным, что у нас одинаково полные данные за весь период?
- Есть ли добавленные данные прошлых периодов?
- Определим, с какого момента данные полные и отбросим более старые.
- Данными за какой период мы располагаем на самом деле?
- Много ли событий и пользователей мы потеряли, отбросив старые данные?
- Проверим, что у нас есть пользователи из всех трёх экспериментальных групп.
Шаг 4. Изучим воронку событий
- Посмотрим, какие события есть в логах, как часто они встречаются;
- Посчитаем, сколько пользователей совершали каждое из этих событий;
- Посчитаем долю пользователей, которые хоть раз совершали событие;
- Проясним, в каком порядке происходят события и какие из них нужно учитывать в воронке приложения;
- Посчитаем конверсию количества пользователей между этапами воронки приложения;
- Поймем, на каком шаге теряем больше всего пользователей;
- Вычислим, какая доля пользователей доходит от первого события до оплаты.
Шаг 5. Изучим результаты эксперимента
- Посчитаем количество пользователей в каждой экспериментальной группе и объединенной контрольной группе;
- Выберем уровень статистической значимости для сравнения четырех гипотез с учетом поправки Бонферрони;
- Контрольные группы:
- Проверим наличие двух контрольных групп, чтобы проверить корректность всех механизмов и расчётов;
- Проверим, находят ли статистические критерии разницу между выборками контрольных групп;
- Самое популярное событие:
- Выберем самое популярное событие;
- Посчитаем число пользователей, совершивших это событие в каждой из контрольных групп;
- Посчитаем долю пользователей, совершивших это событие;
- Проверим, будет ли различие между группами статистически достоверным. Для расчетов создадим функцию.
- Прочие события воронки:
- Посчитаем число пользователей, совершивших это событие в каждой из контрольных групп;
- Посчитаем долю пользователей, совершивших это событие;
- Проверим, будет ли различие между группами статистически достоверным;
- Сделаем вывод о том, работает ли разбиение на группы корректно;
- Аналогично поступим с группой с изменённым шрифтом.
- Сравним группу B с каждой из контрольных групп, а также с объединенной контрольной группой.
- Сравнение выполним по каждому из этапов воронки.
- Выясним, будет ли различие между группами статистически достоверным.
- Сравним группу B с каждой из контрольных групп, а также с объединенной контрольной группой.
- Сделаем выводы из эксперимента.
Шаг 1. Получим данные¶
Подключим библиотеки.
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as st
import datetime as dt
import numpy as np
import math as mth
import plotly.express as px
Откроем файл с данными и изучим общую информацию.
try:
df0 = pd.read_csv('datasets/logs_exp.csv', sep='\t')
except:
print('Файл не загружен. Проверьте путь.')
df0.head(3)
| EventName | DeviceIDHash | EventTimestamp | ExpId | |
|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 |
Описание данных¶
Каждая запись в логе — это действие пользователя, или событие.
EventName— название события;DeviceIDHash— уникальный идентификатор пользователя;EventTimestamp— время события;ExpId— номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.
Создадим копию датафрейма.
df = df0.copy()
Выведем общую информацию.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EventName 244126 non-null object 1 DeviceIDHash 244126 non-null int64 2 EventTimestamp 244126 non-null int64 3 ExpId 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
Всего 244 125 записей в 4 столбцах.
Шаг 2. Подготовим данные¶
Переименуем столбцы для удобства работы.
df.rename(columns={ 'EventName': 'event_name', 'DeviceIDHash': 'user_id',
'EventTimestamp':'event_timestamp', 'ExpId':'exp_id'}, inplace=True)
Посчитаем количество пропусков.
df.isna().sum()
event_name 0 user_id 0 event_timestamp 0 exp_id 0 dtype: int64
В данных отсутствуют пропуски.
Добавим столбец event_dt с датой и временем события и отдельный столбец event_date просто с датой.
df['event_dt'] = pd.to_datetime(df['event_timestamp'], unit='s')
df['event_date'] = pd.to_datetime(df['event_dt'].dt.date)
Проверим полученный результат.
df.head(3)
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | |
|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 |
Добавим столбец с удобным названием группы тестирования.
# Справочник названий групп
test_groups_names = {246: 'A1', 247: 'A2', 248: 'B'}
df['group'] = df['exp_id'].map(test_groups_names)
df.head()
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | group | |
|---|---|---|---|---|---|---|---|
| 0 | MainScreenAppear | 4575588528974610257 | 1564029816 | 246 | 2019-07-25 04:43:36 | 2019-07-25 | A1 |
| 1 | MainScreenAppear | 7416695313311560658 | 1564053102 | 246 | 2019-07-25 11:11:42 | 2019-07-25 | A1 |
| 2 | PaymentScreenSuccessful | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 | B |
| 3 | CartScreenAppear | 3518123091307005509 | 1564054127 | 248 | 2019-07-25 11:28:47 | 2019-07-25 | B |
| 4 | PaymentScreenSuccessful | 6217807653094995999 | 1564055322 | 248 | 2019-07-25 11:48:42 | 2019-07-25 | B |
Проверим полученные типы данных.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 244126 non-null object 1 user_id 244126 non-null int64 2 event_timestamp 244126 non-null int64 3 exp_id 244126 non-null int64 4 event_dt 244126 non-null datetime64[ns] 5 event_date 244126 non-null datetime64[ns] 6 group 244126 non-null object dtypes: datetime64[ns](2), int64(3), object(2) memory usage: 13.0+ MB
Проверим данные на наличие дубликатов.
df.duplicated().sum()
413
Обнаружено 413 дубликатов. Удалим их.
df.drop_duplicates(inplace=True)
Проверим результат.
df.duplicated().sum()
0
Дубликаты удалены.
Проверим, что пользователи присутствуют только в одной из групп теста.
Пользователи группы А1.
group_a1_users_id = pd.DataFrame(df.query('group == "A1"')['user_id'].unique())
group_a1_users_id.columns=['user_id']
group_a1_users_id.head(3)
| user_id | |
|---|---|
| 0 | 4575588528974610257 |
| 1 | 7416695313311560658 |
| 2 | 8351860793733343758 |
Пользователи группы А2.
group_a2_users_id = pd.DataFrame(df.query('group == "A2"')['user_id'].unique())
group_a2_users_id.columns=['user_id']
group_a2_users_id.head(3)
| user_id | |
|---|---|
| 0 | 1850981295691852772 |
| 1 | 948465712512390382 |
| 2 | 2140904690380565988 |
Пользователи группы B.
group_b_users_id = pd.DataFrame(df.query('group == "B"')['user_id'].unique())
group_b_users_id.columns=['user_id']
group_b_users_id.head(3)
| user_id | |
|---|---|
| 0 | 3518123091307005509 |
| 1 | 6217807653094995999 |
| 2 | 2547684315586332355 |
Проверим пересечения идентификаторов пользователей.
group_a1_users_id.merge(group_a2_users_id, on='user_id')
| user_id |
|---|
group_a1_users_id.merge(group_b_users_id, on='user_id')
| user_id |
|---|
group_a2_users_id.merge(group_b_users_id, on='user_id')
| user_id |
|---|
Все пользователи принадлежат только к одной из групп теста.
Вывод. Шаг 2. Подготовим данные¶
Данные подготовлены к работе. В данных отсутствовали пропуски.
- Выполнено переименование столбцов;
- Добавлен столбец с датой и временем события
event_dt; - Добавлен столбец с датой события
event_date; - Удалено 413 дубликатов.
- Пересечений идентификаторов пользователей между группами теста не выявлено.
Шаг 3. Изучим и проверим данные¶
Количество событий в логе.
events_before_deletion = df.shape[0]
print('Количество событий в выборке:', events_before_deletion)
Количество событий в выборке: 243713
Количества пользователей в логе.
users_before_deletion = df['user_id'].nunique()
print('Количество пользователей в выборке:', users_before_deletion)
Количество пользователей в выборке: 7551
Среднее, минимальное, максимальное количество событий на пользователя.
events_per_user = df.groupby('user_id').agg({'event_name': 'count'})
print(
f'Среднее количество событий на пользователя: {events_per_user.event_name.mean():0.1f};\n'
f'Минимальное количество событий на пользователя: {events_per_user.event_name.min()};\n',
f'Максимальное количество событий на пользователя: {events_per_user.event_name.max()}.'
)
Среднее количество событий на пользователя: 32.3; Минимальное количество событий на пользователя: 1; Максимальное количество событий на пользователя: 2307.
В среднем на пользователя приходится 32.3 события, максимум 2307. В данных присутствуют выбросы по количеству событий.
Период, за который мы располагаем данными.
Определим минимальную и максимальную даты.
df.pivot_table(index='group', values='event_date', aggfunc=['min', 'max'])
| min | max | |
|---|---|---|
| event_date | event_date | |
| group | ||
| A1 | 2019-07-25 | 2019-08-07 |
| A2 | 2019-07-25 | 2019-08-07 |
| B | 2019-07-25 | 2019-08-07 |
Во всех группах тестирования представлены данные за период с 25 июля по 7 августа 2019 года.
Посчитаем количество записей о событиях в зависимости от даты и группы тестирования.
result = df.pivot_table(index='event_date', columns='group', values='user_id', aggfunc='count')
result
| group | A1 | A2 | B |
|---|---|---|---|
| event_date | |||
| 2019-07-25 | 4 | 1 | 4 |
| 2019-07-26 | 14 | 8 | 9 |
| 2019-07-27 | 24 | 23 | 8 |
| 2019-07-28 | 33 | 36 | 36 |
| 2019-07-29 | 55 | 58 | 71 |
| 2019-07-30 | 129 | 138 | 145 |
| 2019-07-31 | 620 | 664 | 746 |
| 2019-08-01 | 11561 | 12306 | 12274 |
| 2019-08-02 | 10946 | 10990 | 13618 |
| 2019-08-03 | 10575 | 11024 | 11683 |
| 2019-08-04 | 11514 | 9942 | 11512 |
| 2019-08-05 | 12368 | 10949 | 12741 |
| 2019-08-06 | 11726 | 11720 | 12342 |
| 2019-08-07 | 10612 | 10091 | 10393 |
31 июля количество событий существенно возрастает. С 1 августа количество событий увеличивается на два порядка.
Построим график количества событий по датам для каждой группы тестирования.
result.plot()
plt.ylabel('Количество событий')
plt.title('Количество событий по группам тестирования и датам')
plt.show()
С 1 по 6 августа событий в группе B стабильно больше, чем в контрольных группах.
ax, fig = plt.subplots(figsize=(10,5))
X_axis = np.arange(result.shape[0])
rects = plt.bar(X_axis - 0.2, result['A1'], 0.2, label = 'A1')
plt.bar(X_axis, result['A2'], 0.2, label = 'A2')
plt.bar(X_axis + 0.2, result['B'], 0.2, label = 'B')
plt.xlabel('Даты')
plt.ylabel('Количество событий')
plt.title('Количество событий по группам')
plt.legend()
plt.show()
В минимальном количестве события тестирования возникают с 25 июля 2019 года.
Вероятно, первая неделя была посвящена проверке работы инструментов для сбора статистики, подтверждения корректности разделения пользователей на группы.
С 1 августа началось активное привлечение трафика, количество событий возросло на два порядка.
Тест продлился до 7 августа 2019 года.
Количество событий по группам соизмеримо.
Технически в логи новых дней по некоторым пользователям могут «доезжать» события из прошлого — это может «перекашивать данные». Проверим данные на такой артефакт.
Посмотрим на события за 31 июля в разбивке по часам.
result = df.loc[df['event_date'] == "2019-07-31"].sort_values(by=['user_id', 'event_dt'])
result['hour'] = result['event_dt'].dt.round('1h')
result.head()
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | group | hour | |
|---|---|---|---|---|---|---|---|---|
| 1908 | MainScreenAppear | 33176906322804559 | 1564601363 | 248 | 2019-07-31 19:29:23 | 2019-07-31 | B | 2019-07-31 19:00:00 |
| 1806 | OffersScreenAppear | 33589551945846495 | 1564597381 | 248 | 2019-07-31 18:23:01 | 2019-07-31 | B | 2019-07-31 18:00:00 |
| 1783 | MainScreenAppear | 38880205595577265 | 1564596510 | 248 | 2019-07-31 18:08:30 | 2019-07-31 | B | 2019-07-31 18:00:00 |
| 1418 | MainScreenAppear | 50002244844355989 | 1564584723 | 246 | 2019-07-31 14:52:03 | 2019-07-31 | A1 | 2019-07-31 15:00:00 |
| 2584 | MainScreenAppear | 65356103704674532 | 1564611872 | 246 | 2019-07-31 22:24:32 | 2019-07-31 | A1 | 2019-07-31 22:00:00 |
Посчитаем количество событий за каждый час.
result_agg = result.groupby('hour').agg({'user_id': 'count'})
result_agg.columns = ['events']
result_agg.reset_index(inplace=True)
result_agg['hour'] = pd.DatetimeIndex(result_agg['hour']).hour
result_agg.tail(7)
| hour | events | |
|---|---|---|
| 17 | 18 | 96 |
| 18 | 19 | 92 |
| 19 | 20 | 55 |
| 20 | 21 | 258 |
| 21 | 22 | 393 |
| 22 | 23 | 178 |
| 23 | 0 | 33 |
Построим график количества событий по часам за 31 июля.
plt.bar(result_agg['hour'], result_agg['events'])
plt.title('Количество событий в час за 31 июля 2019 года')
plt.xlabel('Час')
plt.ylabel('Количество событий')
plt.show()
С 21 часа 31 июля 2019 года происходит существенное увеличение количества событий.
Видимо, до этого шла проверка работы теста. С 21:00 началось активное привлечение пользователей.
Уберем из выборки все события ранее 21:00 31 июля 2019 года.
df = df[df['event_dt'] >= '2019-07-31 21:00:00']
df.head(3)
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | group | |
|---|---|---|---|---|---|---|---|
| 1990 | MainScreenAppear | 7701922487875823903 | 1564606857 | 247 | 2019-07-31 21:00:57 | 2019-07-31 | A2 |
| 1991 | MainScreenAppear | 2539077412200498909 | 1564606905 | 247 | 2019-07-31 21:01:45 | 2019-07-31 | A2 |
| 1992 | OffersScreenAppear | 3286987355161301427 | 1564606941 | 248 | 2019-07-31 21:02:21 | 2019-07-31 | B |
Самое раннее событие в выборке.
df['event_dt'].min()
Timestamp('2019-07-31 21:00:57')
Посчитаем количество событий после удаления данных.
events_after_deletion = df.shape[0]
print('Количество событий после удаления данных:', events_after_deletion,
'\nОтброшено', (events_before_deletion - events_after_deletion),
'событий, что составляет ', '{:.1%}'.format(
(events_before_deletion - events_after_deletion) / events_before_deletion), 'от общего числа.'
)
Количество событий после удаления данных: 241724 Отброшено 1989 событий, что составляет 0.8% от общего числа.
Посчитаем количество пользователей после удаления данных.
users_after_deletion = df['user_id'].nunique()
print('Количество пользователей после удаления данных:', users_after_deletion,
'\nОтброшено', (users_before_deletion - users_after_deletion),
'пользователей, что составляет ', '{:.1%}'.format(
(users_before_deletion - users_after_deletion) / users_before_deletion), 'от общего числа.'
)
Количество пользователей после удаления данных: 7538 Отброшено 13 пользователей, что составляет 0.2% от общего числа.
Теперь мы уверены, что доля мала.
Сопоставим количество пользователей в каждой из экспериментальных групп.
ratio = df.pivot_table(index='group', values='user_id', aggfunc='nunique')
ratio['ratio'] = ratio['user_id'] / ratio['user_id'].sum()
ratio.columns = ['users', 'ratio']
ratio.style.format({'ratio': '{0:.2%}'})
| users | ratio | |
|---|---|---|
| group | ||
| A1 | 2484 | 32.95% |
| A2 | 2517 | 33.39% |
| B | 2537 | 33.66% |
Всего пользователей в группе A1 - 2484 человека (32.95%), A2 - 2517 (33.39%), B - 2537 (33.66%).
За какой период мы располагаем данными на самом деле.
df.pivot_table(index='group', values='event_date', aggfunc=['min', 'max'])
| min | max | |
|---|---|---|
| event_date | event_date | |
| group | ||
| A1 | 2019-07-31 | 2019-08-07 |
| A2 | 2019-07-31 | 2019-08-07 |
| B | 2019-07-31 | 2019-08-07 |
В каждой из трех экспериментальных групп пользователи есть. Количество сопоставимо.
Мы убедились, что пользователи сохранены по каждой из трех групп после фильтрации. Приступим к изучению воронки.
Вывод. Шаг 3. Изучим и проверим данные¶
Всего в логе 243 713 уникальных события от 7551 уникального пользователя.
В среднем на пользователя приходится 32.3 события, максимум 2307 шт.,
то есть в данных присутствуют выбросы по количеству событий.
Во всех группах тестирования представлены данные за период с 25 июля по 7 августа 2019 года.
В минимальном количестве события тестирования возникают с 25 июля 2019 года.
Первая неделя была посвящена проверке работы инструментов для сбора статистики, подтверждения корректности разделения пользователей на группы.
С 31 июля 21:00 началось активное привлечение трафика.
Тест продлился до 7 августа 2019 года.
Количество событий по группам соизмеримо.
Данные прошлых периодов мы удалили из выборки.
Удалено 1989 событий (0.8%) и 13 пользователей (0.2%).
Полными данные можно считать с 31 июля по 7 августа 2019 года.
Количество событий после удаления данных: 241724
Количество пользователей после удаления данных: 7538
Всего пользователей в группе A1 - 2484 человека (32.95%), A2 - 2517 (33.39%), B - 2537 (33.66%).
В каждой из трех экспериментальных групп пользователи есть. Количество сопоставимо.
Шаг 4. Воронка событий¶
События в логах
Посмотрим, какие события есть в логах, как часто они встречаются.
funnel = df.pivot_table(index='event_name', values='user_id', aggfunc={'count'})
funnel['ratio'] = funnel['count'] / funnel['count'].sum()
funnel.sort_values(by='ratio', ascending=False, inplace=True)
funnel.style.format({'ratio': '{0:.1%}'})
| count | ratio | |
|---|---|---|
| event_name | ||
| MainScreenAppear | 117889 | 48.8% |
| OffersScreenAppear | 46531 | 19.2% |
| CartScreenAppear | 42343 | 17.5% |
| PaymentScreenSuccessful | 33951 | 14.0% |
| Tutorial | 1010 | 0.4% |
В перечне событий видим:
- MainScreenAppear (Появится главный экран) - 48.8% от общего числа событий в логах;
- OffersScreenAppear (Появится экран предложений) - 19.2%;
- CartScreenAppear (Появится экран корзины) - 17.5%;
- PaymentScreenSuccessful (Экран оплаты прошел успешно) - 14.0%;
- Tutorial (Руководство) - 0.4%.
Сколько пользователей совершали каждое из этих событий
funnel_users = df.pivot_table(index='event_name', values='user_id', aggfunc={'nunique'})
funnel_users['ratio'] = funnel_users['nunique'] / funnel_users['nunique'].sum()
funnel_users.sort_values(by='nunique', ascending=False, inplace=True)
funnel_users.style.format({'ratio': '{0:.1%}'})
| nunique | ratio | |
|---|---|---|
| event_name | ||
| MainScreenAppear | 7423 | 36.9% |
| OffersScreenAppear | 4597 | 22.8% |
| CartScreenAppear | 3736 | 18.6% |
| PaymentScreenSuccessful | 3540 | 17.6% |
| Tutorial | 843 | 4.2% |
Количество уникальных пользователей, хоть раз совершивших событие, и доля этих пользователей от общего числа:
MainScreenAppear (Появится Главный экран) - 7423 чел., 36.9%;
OffersScreenAppear (Появится экран предложений) - 4597 чел., 22.8%;
CartScreenAppear (Появится экран корзины) - 3736 чел., 18.6%;
PaymentScreenSuccessful (Экран оплаты прошел успешно) - 3540 чел., 17.6%;
Tutorial (Руководство) - 843 чел., 4.2%.
Порядок событий в воронке продаж
Все события, кроме Tutorial, относятся к воронке продаж. Tutorial - это вспомогательная активность и напрямую к воронке продаж отношения не имеет.
Корректное расположение событий в воронке:
- MainScreenAppear (Появится Главный экран);
- OffersScreenAppear (Появится экран предложений);
- CartScreenAppear (Появится экран корзины);
- PaymentScreenSuccessful (Экран оплаты прошел успешно).
Объявим список с перечнем этапов воронки.
funnel_names = ['MainScreenAppear', 'OffersScreenAppear', 'CartScreenAppear', 'PaymentScreenSuccessful']
Отобразим воронку событий на графике.
result = funnel[funnel.index != 'Tutorial'].reset_index()
fig = px.funnel(result, x='count', y='event_name')
fig.update_layout(
autosize=False,
width=800,
height=350,
)
fig.update_layout(title='Воронка событий', title_x = 0.5)
fig.show()
Отобразим воронку количества пользователей на графике.
result = funnel_users[funnel_users.index != 'Tutorial'].reset_index()
fig = px.funnel(result, x='nunique', y='event_name')
fig.update_layout(
autosize=False,
width=800,
height=350,
)
fig.update_layout(title='Воронка пользователей', title_x = 0.5)
fig.show()
Создадим столбец с номером этапа воронки продаж.
df['funnel'] = df['event_name'].map({'MainScreenAppear': 1,
'OffersScreenAppear': 2,
'CartScreenAppear': 3,
'PaymentScreenSuccessful': 4})
df.head(5)
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | group | funnel | |
|---|---|---|---|---|---|---|---|---|
| 1990 | MainScreenAppear | 7701922487875823903 | 1564606857 | 247 | 2019-07-31 21:00:57 | 2019-07-31 | A2 | 1.0 |
| 1991 | MainScreenAppear | 2539077412200498909 | 1564606905 | 247 | 2019-07-31 21:01:45 | 2019-07-31 | A2 | 1.0 |
| 1992 | OffersScreenAppear | 3286987355161301427 | 1564606941 | 248 | 2019-07-31 21:02:21 | 2019-07-31 | B | 2.0 |
| 1993 | OffersScreenAppear | 3187166762535343300 | 1564606943 | 247 | 2019-07-31 21:02:23 | 2019-07-31 | A2 | 2.0 |
| 1994 | MainScreenAppear | 1118952406011435924 | 1564607005 | 248 | 2019-07-31 21:03:25 | 2019-07-31 | B | 1.0 |
Посчитаем конверсию количества пользователей между этапами воронки продаж.
funnel_cr = df[df['funnel'].notna()].pivot_table(index=['funnel', 'event_name'],
values='user_id', aggfunc='nunique')
funnel_cr.columns = ['users']
funnel_cr['cr_previous'] = funnel_cr['users'] / funnel_cr['users'].shift(1)
funnel_cr['cr_pay'] = funnel_cr['users'] / funnel_cr['users'].shift(3)
funnel_cr.style.format({'cr_previous': '{0:.1%}', 'cr_pay': '{0:.1%}'})
| users | cr_previous | cr_pay | ||
|---|---|---|---|---|
| funnel | event_name | |||
| 1.000000 | MainScreenAppear | 7423 | nan% | nan% |
| 2.000000 | OffersScreenAppear | 4597 | 61.9% | nan% |
| 3.000000 | CartScreenAppear | 3736 | 81.3% | nan% |
| 4.000000 | PaymentScreenSuccessful | 3540 | 94.8% | 47.7% |
Конверсия пользователей:
- с этапа воронки MainScreenAppear на OffersScreenAppear - 61.9%;
- с этапа OffersScreenAppear в CartScreenAppear - 81.3%;
- с этапа CartScreenAppear в PaymentScreenSuccessful - 94.8%.
Больше всего пользователей теряется при переходе с первого шага на второй (с главного экрана на экран с предложениями).
Потери составляют 38.1%.
До этапа оплаты доходит 47.7% уникальных пользователей, увидевших главный экран.
Проверим, все ли пользователи, которые есть на поздних этапах воронки, присутствуют на более ранних этапах.
for funnel_l in range(1,5):
for funnel_r in range(funnel_l+1, 5):
user_id_funnel_l = df.loc[df['funnel'] == funnel_l, 'user_id']
user_id_funnel_r = df.loc[df['funnel'] == funnel_r, 'user_id']
lost_users_cnt = user_id_funnel_r[~user_id_funnel_r.isin(user_id_funnel_l)].shape[0]
print(funnel_l, funnel_r, 'Потерялись:', lost_users_cnt)
1 2 Потерялись: 950 1 3 Потерялись: 911 1 4 Потерялись: 747 2 3 Потерялись: 119 2 4 Потерялись: 16 3 4 Потерялись: 5
Есть пользователи, которые отсутствуют на ранних этапах воронки, но присутствуют на более поздних.
Пользователи, которые не прошли MainScreenAppear есть в OffersScreenAppear 950 человек,
CartScreenAppear 911 человек, PaymentScreenSuccessful 747 человек.
Пользователи, которые не прошли OffersScreenAppear есть в CartScreenAppear 119 человек, PaymentScreenSuccessful 16 человек.
Пользователи, которые не прошли CartScreenAppear есть в PaymentScreenSuccessful 5 человек.
Таким образом, дизайн приложения позволяет выполнять покупки и оплаты, минуя главный экран и другие промежуточные экраны.
Вывод. Шаг 4. Воронка событий¶
В перечне событий видим:
- MainScreenAppear (Появится главный экран) - 48.8% от общего числа событий в логах;
- OffersScreenAppear (Появится экран предложений) - 19.2%;
- CartScreenAppear (Появится экран корзины) - 17.5%;
- PaymentScreenSuccessful (Экран оплаты прошел успешно) - 14.0%;
- Tutorial (Руководство) - 0.4%.
Количество уникальных пользователей, хоть раз совершивших событие, и доля этих пользователей от общего числа:
MainScreenAppear (Появится Главный экран) - 7423 чел., 36.9%;
OffersScreenAppear (Появится экран предложений) - 4597 чел., 22.8%;
CartScreenAppear (Появится экран корзины) - 3736 чел., 18.6%;
PaymentScreenSuccessful (Экран оплаты прошел успешно) - 3540 чел., 17.6%;
Tutorial (Руководство) - 843 чел., 4.2%.
Все события, кроме Tutorial, относятся к воронке продаж. Tutorial - это вспомогательная активность и напрямую к воронке продаж отношения не имеет.
Корректное расположение событий в воронке:
- MainScreenAppear (Появится Главный экран);
- OffersScreenAppear (Появится экран предложений);
- CartScreenAppear (Появится экран корзины);
- PaymentScreenSuccessful (Экран оплаты прошел успешно).
Конверсия пользователей:
- с этапа воронки MainScreenAppear на OffersScreenAppear - 61.9%;
- с этапа OffersScreenAppear в CartScreenAppear - 81.3%;
- с этапа CartScreenAppear в PaymentScreenSuccessful - 94.8%.
Больше всего пользователей теряется при переходе с первого шага на второй (с главного экрана на экран с предложениями).
Потери составляют 38.1%.
До этапа оплаты доходит 47.7% уникальных пользователей, увидевших главный экран.
Есть пользователи, которые отсутствуют на ранних этапах воронки, но присутствуют на более поздних.
Пользователи, которые не прошли MainScreenAppear есть в OffersScreenAppear 950 человек,
CartScreenAppear 911 человек, PaymentScreenSuccessful 747 человек.
Пользователи, которые не прошли OffersScreenAppear есть в CartScreenAppear 119 человек, PaymentScreenSuccessful 16 человек.
Пользователи, которые не прошли CartScreenAppear есть в PaymentScreenSuccessful 5 человек.
Таким образом, дизайн приложения позволяет выполнять покупки и оплаты, минуя главный экран и другие промежуточные экраны.
Обнаруженная проблема — провал на первом шаге: от MainScreenAppear к OffersScreenAppear. Вероятно, нужно лучше прорабатывать механику приложения, чтобы пользователи переходили к OffersScreen.
Шаг 5. Результаты эксперимента¶
Уровень статистической значимости¶
Выберем уровень статистической значимости для сравнения четырех гипотез с учетом поправки Бонферрони.
Всего будут сравниваться четыре независимые выборки: A1 / A2 ( 246 / 247 ), A1 / B ( 246 / 248 ), A2 / B ( 247 / 248 ), (A1 + A2) / B ( 246 + 247 / 248 ). Для каждой пары будем сравнивать четыре этапа воронки. Будет применен один статистический критерий. Таким образом, имеем множественное сравнение выборки B с другими независимыми от B выборками.
Количество сравнений.
m = 4 * 4 * 1
m
16
Применим поправку Бонферрони для уточнения критерия статистической значимости.
alpha = 0.05
alpha /= m
print('Принятый критерий статистической значимости с учетом корректировки Бонферрони:', alpha)
Принятый критерий статистической значимости с учетом корректировки Бонферрони: 0.003125
Подготовим данные для проверки гипотез¶
Создадим новый датафрейм, в который добавим данные по объединенной контрольной группе. Для этого отберем данные групп A1, A2 и переименуем название группы в значение A3.
df.shape[0]
241724
df_u = df.query('group == "A1" or group == "A2"').copy()
df_u['group'] = 'A3'
df_u
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | group | funnel | |
|---|---|---|---|---|---|---|---|---|
| 1990 | MainScreenAppear | 7701922487875823903 | 1564606857 | 247 | 2019-07-31 21:00:57 | 2019-07-31 | A3 | 1.0 |
| 1991 | MainScreenAppear | 2539077412200498909 | 1564606905 | 247 | 2019-07-31 21:01:45 | 2019-07-31 | A3 | 1.0 |
| 1993 | OffersScreenAppear | 3187166762535343300 | 1564606943 | 247 | 2019-07-31 21:02:23 | 2019-07-31 | A3 | 2.0 |
| 1996 | OffersScreenAppear | 3511569580412335882 | 1564607172 | 246 | 2019-07-31 21:06:12 | 2019-07-31 | A3 | 2.0 |
| 1997 | OffersScreenAppear | 3511569580412335882 | 1564607236 | 246 | 2019-07-31 21:07:16 | 2019-07-31 | A3 | 2.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 244121 | MainScreenAppear | 4599628364049201812 | 1565212345 | 247 | 2019-08-07 21:12:25 | 2019-08-07 | A3 | 1.0 |
| 244122 | MainScreenAppear | 5849806612437486590 | 1565212439 | 246 | 2019-08-07 21:13:59 | 2019-08-07 | A3 | 1.0 |
| 244123 | MainScreenAppear | 5746969938801999050 | 1565212483 | 246 | 2019-08-07 21:14:43 | 2019-08-07 | A3 | 1.0 |
| 244124 | MainScreenAppear | 5746969938801999050 | 1565212498 | 246 | 2019-08-07 21:14:58 | 2019-08-07 | A3 | 1.0 |
| 244125 | OffersScreenAppear | 5746969938801999050 | 1565212517 | 246 | 2019-08-07 21:15:17 | 2019-08-07 | A3 | 2.0 |
156849 rows × 8 columns
Объединим полученный датафрейм с исходным. Получим набор данных для групп контрольных A1, A2, объединенной контрольной A3 и группы с измененным шрифтом B.
df_u = pd.concat([df_u, df])
df_u
| event_name | user_id | event_timestamp | exp_id | event_dt | event_date | group | funnel | |
|---|---|---|---|---|---|---|---|---|
| 1990 | MainScreenAppear | 7701922487875823903 | 1564606857 | 247 | 2019-07-31 21:00:57 | 2019-07-31 | A3 | 1.0 |
| 1991 | MainScreenAppear | 2539077412200498909 | 1564606905 | 247 | 2019-07-31 21:01:45 | 2019-07-31 | A3 | 1.0 |
| 1993 | OffersScreenAppear | 3187166762535343300 | 1564606943 | 247 | 2019-07-31 21:02:23 | 2019-07-31 | A3 | 2.0 |
| 1996 | OffersScreenAppear | 3511569580412335882 | 1564607172 | 246 | 2019-07-31 21:06:12 | 2019-07-31 | A3 | 2.0 |
| 1997 | OffersScreenAppear | 3511569580412335882 | 1564607236 | 246 | 2019-07-31 21:07:16 | 2019-07-31 | A3 | 2.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 244121 | MainScreenAppear | 4599628364049201812 | 1565212345 | 247 | 2019-08-07 21:12:25 | 2019-08-07 | A2 | 1.0 |
| 244122 | MainScreenAppear | 5849806612437486590 | 1565212439 | 246 | 2019-08-07 21:13:59 | 2019-08-07 | A1 | 1.0 |
| 244123 | MainScreenAppear | 5746969938801999050 | 1565212483 | 246 | 2019-08-07 21:14:43 | 2019-08-07 | A1 | 1.0 |
| 244124 | MainScreenAppear | 5746969938801999050 | 1565212498 | 246 | 2019-08-07 21:14:58 | 2019-08-07 | A1 | 1.0 |
| 244125 | OffersScreenAppear | 5746969938801999050 | 1565212517 | 246 | 2019-08-07 21:15:17 | 2019-08-07 | A1 | 2.0 |
398573 rows × 8 columns
Количество записей соответствует ожидаемому.
Посчитаем количество пользователей в каждой группе.
groups_users = df_u.pivot_table(index='group', values='user_id', aggfunc='nunique')
groups_users.columns = ['users']
groups_users
| users | |
|---|---|
| group | |
| A1 | 2484 |
| A2 | 2517 |
| A3 | 5001 |
| B | 2537 |
Объявим функцию для вывода количества пользователей в соответствующей группе тестирования.
def users_group_cnt(group='A1', data=df_u):
return data.query('group == @group')['user_id'].nunique()
users_group_cnt(group='A1')
2484
users_group_cnt(group='B')
2537
Функция работает корректно.
Объявим функцию для подсчета количества пользователей на соответствующем этапе воронки для выбранной группы A/A/B теста.
def users_funnel_cnt(group='A1', funnel=1, data=df_u):
return data.query('group == @group and funnel == @funnel')['user_id'].nunique()
users_funnel_cnt(group='A1', funnel=1)
2450
users_funnel_cnt(group='A2', funnel=1)
2479
Функция работает корректно.
Сравнение контрольных групп¶
Проверим, находят ли статистические критерии разницу между выборками A1 и A2.
Сравним доли пользователей, которые находятся на определенном этапе воронки продаж, относительно общего числа пользователей этой группы.
Доли пользователей первого этапа воронки для контрольных групп¶
Определим гипотезы:
Нулевая - Доли пользователей первого этапа воронки между контрольными группами A1 A2 равны.
Альтернативная - Доли пользователей первого этапа воронки между контрольными группами A1 A2 статистически значимо отличаются.
Проведем вычисления для первого этапа воронки для контрольных групп.
Объявим переменные для обозначения групп теста и этапа воронки продаж.
group_1 = 'A1'
group_2 = 'A2'
funnel = 1
Количество пользователей в группах.
users_group_cnt(group_1)
2484
users_group_cnt(group_2)
2517
Количество пользователей искомого этапа воронки.
users_funnel_cnt(group_1, funnel)
2450
users_funnel_cnt(group_2, funnel)
2479
Проверим статистическую значимость гипотезы о том, что конверсия обеих групп равна. Применим Z-критерий.
# Выводить ли промежуточные расчеты
diagnostic = False
print('Группы:', group_1, group_2)
print('Этап воронки:', funnel, ' - ', funnel_names[funnel - 1])
successes = np.array([users_funnel_cnt(group_1, funnel), users_funnel_cnt(group_2, funnel)])
trials = np.array([users_group_cnt(group_1), users_group_cnt(group_2)])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
print('Доля пользователей этапа воронки к числу пользователей в группе', p1, p2 )
if diagnostic:
# Выводим диагностические данные
print('p_combined', p_combined)
print('difference', difference)
if difference == 0:
p_value = 1
else:
# считаем статистику в стандартных отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, стандартное отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
if diagnostic:
# Выводим диагностические данные
print('z_value', z_value)
print('distr.cdf(abs(z_value))', distr.cdf(abs(z_value)))
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
Группы: A1 A2 Этап воронки: 1 - MainScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.9863123993558777 0.9849026618990863 p-значение: 0.6756217702005545 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Функции для сравнения выборок¶
Завернем проведенные вычисления в функцию и сравним контрольные выборки по другим этапам воронки продаж.
Функция для расчета Z-критерия.
def z_criterion(group_1 = 'A1', group_2 = 'A2', funnel = 2, diagnostic=False):
''' group_1, group_2 - группы теста, сравниваемые Z-критерием.
funnel - этап воронки, для которого выполняется сравнение групп. '''
print('Группы:', group_1, group_2)
print('Этап воронки:', funnel, ' - ', funnel_names[funnel - 1])
successes = np.array([users_funnel_cnt(group_1, funnel), users_funnel_cnt(group_2, funnel)])
trials = np.array([users_group_cnt(group_1), users_group_cnt(group_2)])
# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# разница пропорций в датасетах
difference = p1 - p2
print('Доля пользователей этапа воронки к числу пользователей в группе', p1, p2 )
if diagnostic:
# Выводим диагностические данные
print('p_combined', p_combined)
print('difference', difference)
if difference == 0:
p_value = 1
else:
# считаем статистику в стандартных отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# задаем стандартное нормальное распределение (среднее 0, стандартное отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
if diagnostic:
# Выводим диагностические данные
print('z_value', z_value)
print('distr.cdf(abs(z_value))', distr.cdf(abs(z_value)))
print('p-значение: ', p_value)
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница \n')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными \n'
)
Сравнение остальных этапов воронки контрольных групп¶
Сравним остальные этапы воронки контрольных групп с помощью Z-критерия.
Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между контрольными группами A1 A2 равны.
Альтернативная - Доли пользователей этапа воронки между контрольными группами A1 A2 статистически значимо отличаются.
for funnel in range(2,5):
z_criterion(group_1='A1', group_2='A2', funnel=funnel)
Группы: A1 A2 Этап воронки: 2 - OffersScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.6207729468599034 0.6054827175208581 p-значение: 0.26698769175859516 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A1 A2 Этап воронки: 3 - CartScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.5096618357487923 0.4922526817640048 p-значение: 0.2182812140633792 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A1 A2 Этап воронки: 4 - PaymentScreenSuccessful Доля пользователей этапа воронки к числу пользователей в группе 0.4830917874396135 0.4600715137067938 p-значение: 0.10298394982948822 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Вывод. Сравнение контрольных групп¶
С помощью статистических критериев не удалось обнаружить различия между тестовыми группами. Разделение на группы работает корректно.
Сравнение группы с измененным шрифтом с первой контрольной группой¶
Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между контрольной группой A1 и группой B равны.
Альтернативная - Доли пользователей этапа воронки между контрольной группой A1 и группой B статистически значимо отличаются.
for funnel in range(1,5):
z_criterion(group_1='A1', group_2='B', funnel=funnel)
Группы: A1 B Этап воронки: 1 - MainScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.9863123993558777 0.9830508474576272 p-значение: 0.34705881021236484 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A1 B Этап воронки: 2 - OffersScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.6207729468599034 0.6034686637761135 p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A1 B Этап воронки: 3 - CartScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.5096618357487923 0.48521876231769806 p-значение: 0.08328412977507749 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A1 B Этап воронки: 4 - PaymentScreenSuccessful Доля пользователей этапа воронки к числу пользователей в группе 0.4830917874396135 0.4659046117461569 p-значение: 0.22269358994682764 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
При сравнении группы с измененным шрифтом с первой контрольной группой статистически значимые отличия не обнаружены.
Сравнение группы с измененным шрифтом со второй контрольной группой¶
Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между контрольной группой A2 и группой B равны.
Альтернативная - Доли пользователей этапа воронки между контрольной группой A2 и группой B статистически значимо отличаются.
for funnel in range(1,5):
z_criterion(group_1='A2', group_2='B', funnel=funnel)
Группы: A2 B Этап воронки: 1 - MainScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.9849026618990863 0.9830508474576272 p-значение: 0.6001661582453706 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A2 B Этап воронки: 2 - OffersScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.6054827175208581 0.6034686637761135 p-значение: 0.8835956656016957 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A2 B Этап воронки: 3 - CartScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.4922526817640048 0.48521876231769806 p-значение: 0.6169517476996997 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A2 B Этап воронки: 4 - PaymentScreenSuccessful Доля пользователей этапа воронки к числу пользователей в группе 0.4600715137067938 0.4659046117461569 p-значение: 0.6775413642906454 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
При сравнении группы с измененным шрифтом со второй контрольной группой статистически значимые отличия не обнаружены.
Сравнение группы с измененным шрифтом с объединенной контрольной группой¶
Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между объединенной контрольной группой A1+A2 и группой B равны.
Альтернативная - Доли пользователей этапа воронки между объединенной контрольной группой A1+A2 и группой B статистически значимо отличаются.
for funnel in range(1,5):
z_criterion(group_1='A3', group_2='B', funnel=funnel)
Группы: A3 B Этап воронки: 1 - MainScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.9856028794241152 0.9830508474576272 p-значение: 0.39298914928006035 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A3 B Этап воронки: 2 - OffersScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.6130773845230953 0.6034686637761135 p-значение: 0.418998284007599 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A3 B Этап воронки: 3 - CartScreenAppear Доля пользователей этапа воронки к числу пользователей в группе 0.5008998200359928 0.48521876231769806 p-значение: 0.19819340844527744 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Группы: A3 B Этап воронки: 4 - PaymentScreenSuccessful Доля пользователей этапа воронки к числу пользователей в группе 0.471505698860228 0.4659046117461569 p-значение: 0.6452057673098244 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
При сравнении группы с измененным шрифтом с контрольными группами статистически значимые отличия не обнаружены.
Применен критерий статистической значимости с учетом корректировки Бонферрони: 0.003125.
Выполнено 16 проверок.
Полученный уровень значимости:
0.003125 * 16
0.05
Вывод. Шаг 5. Результаты эксперимента¶
В ходе исследования мы выяснили, как ведут себя пользователи мобильного приложения:
- Изучили воронку продаж.
- Узнали, как пользователи доходят до покупки;
- Подсчитали количество пользователей, которые доходят до покупки, и количество тех, кто «застревает» на предыдущих шагах. Определили на каких именно.
- Исследовали результаты A/A/B-эксперимента, посвященного выбору лучшего шрифта.
Была выполнена подготовка данных к работе:
- Выполнили переименование столбцов;
- Добавили столбец с датой и временем события 'event_dt';
- Добавили столбец с датой события 'event_date';
- Удалили 413 дубликатов.
- Пересечений идентификаторов пользователей между группами теста не выявили.
Всего в собранных данных 243713 уникальных события от 7551 уникального пользователя за период с 25 июля по 7 августа 2019 года.
В среднем на пользователя приходится 32.3 события, максимум 2307 шт.
Количество событий по группам соизмеримо.
В выборке присутствовали данные прошлых периодов. Их мы удалили - 1989 событий (0.8%) и 13 пользователей (0.2%).
Полными можно считать данные с 31 июля 21:00 по 7 августа 2019 года.
После удаления данных количество событий 241724, пользователей 7538.
Всего пользователей в группе A1 - 2484 человека (32.95%), A2 - 2517 (33.39%), B - 2537 (33.66%).
В каждой из трех экспериментальных групп пользователи есть. Количество сопоставимо.
Результаты исследования воронки продаж:
В перечне событий видим:
- MainScreenAppear (Появится главный экран) - 48.8% от общего числа событий в логах;
- OffersScreenAppear (Появится экран предложений) - 19.2%;
- CartScreenAppear (Появится экран корзины) - 17.5%;
- PaymentScreenSuccessful (Экран оплаты прошел успешно) - 14.0%;
- Tutorial (Руководство) - 0.4%.
Количество уникальных пользователей, хоть раз совершивших событие, и доля этих пользователей от общего числа:
MainScreenAppear (Появится Главный экран) - 7423 чел., 36.9%;
OffersScreenAppear (Появится экран предложений) - 4597 чел., 22.8%;
CartScreenAppear (Появится экран корзины) - 3736 чел., 18.6%;
PaymentScreenSuccessful (Экран оплаты прошел успешно) - 3540 чел., 17.6%;
Tutorial (Руководство) - 843 чел., 4.2%.
Все события, кроме Tutorial, относятся к воронке продаж. Tutorial - это вспомогательная активность и напрямую к воронке продаж отношения не имеет.
Корректное расположение событий в воронке:
- MainScreenAppear (Появится Главный экран);
- OffersScreenAppear (Появится экран предложений);
- CartScreenAppear (Появится экран корзины);
- PaymentScreenSuccessful (Экран оплаты прошел успешно).
Конверсия пользователей:
- с этапа воронки MainScreenAppear на OffersScreenAppear - 61.9%;
- с этапа OffersScreenAppear в CartScreenAppear - 81.3%;
- с этапа CartScreenAppear в PaymentScreenSuccessful - 94.8%.
Больше всего пользователей теряется при переходе с первого шага на второй (с главного экрана на экран с предложениями).
Потери составляют 38.1%.
До этапа оплаты доходит 47.7% уникальных пользователей, увидевших главный экран.
Таким образом, обнаружена проблема — провал на первом шаге воронки: от MainScreenAppear к OffersScreenAppear.
Вероятно, нужно лучше прорабатывать механику приложения, чтобы пользователи переходили к OffersScreen.
Для сравнения долей пользователей между группами A/A/B теста на этапах воронки был применен Z-критерий.
Выполнено 16 сравнений независимых выборок.
Приняли критерий статистической значимости с учетом корректировки Бонферрони: 0.003125.
Проведено сравнение двух контрольных групп A1 (246) и A2 (247).
Выявили, что между контрольными группами нет статистически значимого различия долей пользователей, то есть сбор данных корректен.
Этап воронки - p-значение:
- MainScreenAppear - 0.6756217702005545
- OffersScreenAppear - 0.26698769175859516
- CartScreenAppear - 0.2182812140633792
- PaymentScreenSuccessful - 0.10298394982948822
Затем аналогичным образом провели сравнение первой контрольной группы A1 (246) и группы с измененным шрифтом B (248).
Статистически значимых отличий в доле пользователей на этапах воронки не выявили.
Этап воронки - p-значение:
- MainScreenAppear - 0.34705881021236484
- OffersScreenAppear - 0.20836205402738917
- CartScreenAppear - 0.08328412977507749
- PaymentScreenSuccessful - 0.22269358994682742
Далее провели сравнение второй контрольной группы A2 (247) и группы с измененным шрифтом B (248).
Статистически значимых отличий в доле пользователей на этапах воронки не выявили.
Этап воронки - p-значение:
- MainScreenAppear - 0.6001661582453706
- OffersScreenAppear - 0.8835956656016957
- CartScreenAppear - 0.6169517476996997
- PaymentScreenSuccessful - 0.6775413642906454
На финальном этапе провели сравнение объединенной контрольной группы A3 (246+247) с группой с измененным шрифтом B (248).
Статистически значимых отличий в доле пользователей на этапах воронки не выявили.
Этап воронки - p-значение:
- MainScreenAppear - 0.39298914928006035
- OffersScreenAppear - 0.418998284007599
- CartScreenAppear - 0.19819340844527744
- PaymentScreenSuccessful - 0.6452057673098244
На основе проведенных тестов нельзя сказать, что изменение шрифта положительно скажется на продажах в приложении.
Доработка признана нецелесообразной.